Redux
1. 서론: 상태 관리의 복잡성과 Redux의 탄생
1.1 현대 웹 애플리케이션의 상태(State) 정의 및 관리의 난제
현대 웹 애플리케이션은 사용자와의 상호작용을 통해 동적으로 변화하는 수많은 데이터 조각들로 구성된다. 이러한 데이터의 집합을 ’상태(state)’라고 정의한다.1 애플리케이션의 규모가 작을 때는 각 컴포넌트가 자신의 지역 상태(local state)를 독립적으로 관리하는 것으로 충분하다. 그러나 애플리케이션의 규모가 커지고 기능이 복잡해짐에 따라, 여러 컴포넌트가 동일한 상태를 공유하고 수정해야 하는 상황이 빈번하게 발생한다.2
이러한 전역적 상태(global state)를 관리하는 것은 상당한 난제이다. 가장 기본적인 접근법은 상위 컴포넌트가 상태를 소유하고, 이를 하위 컴포넌트로 props를 통해 전달하는 방식이다. 하지만 컴포넌트 계층 구조가 깊어지면, 특정 상태가 필요하지 않은 중간 컴포넌트들이 단지 데이터를 전달하기 위한 통로 역할만 하게 되는 ‘Prop Drilling’ 현상이 발생한다.2 이 방식은 코드의 가독성을 심각하게 저해하고, 상태의 출처와 흐름을 추적하기 어렵게 만들어 유지보수 비용을 급격히 증가시킨다.
1.2 전통적 MVC(Model-View-Controller) 패턴의 한계
이러한 상태 관리의 복잡성을 해결하기 위해 과거 프론트엔드 개발에서는 MVC(Model-View-Controller) 패턴이 널리 사용되었다.3 MVC 패턴은 애플리케이션의 구조를 세 가지 주요 구성 요소로 분리한다.
-
Model: 애플리케이션의 데이터와 비즈니스 로직을 담당한다. 데이터의 구조와 유효성을 관리한다.3
-
View: 사용자에게 보여지는 UI(User Interface)를 담당한다. 모델의 데이터를 시각적으로 표현한다.3
-
Controller: 사용자의 입력을 받아 모델의 상태를 변경하거나 뷰를 업데이트하는 역할을 수행한다.3
초기에는 이 구조가 관심사를 분리하는 데 효과적이었으나, 애플리케이션이 고도화되면서 한계에 부딪혔다. 하나의 모델이 여러 뷰에 영향을 주고, 하나의 뷰가 여러 모델의 데이터를 조작하는 다대다(many-to-many) 관계가 형성되면서 데이터 흐름이 양방향(bidirectional)으로 복잡하게 얽히기 시작했다.3 이러한 구조에서는 특정 상태 변화가 어떤 예기치 않은 부수 효과(side effect)를 연쇄적으로 일으킬지 예측하기가 극도로 어려워진다. 버그가 발생했을 때, 그 원인이 되는 상태 변경을 추적하는 과정은 미로를 헤매는 것과 같아졌고, 이는 개발 생산성을 크게 저하시키는 요인이 되었다.3
1.3 패러다임의 전환: Flux 아키텍처의 등장
MVC 패턴의 예측 불가능성을 해결하기 위해 페이스북(Facebook)은 2014년, 새로운 패러다임인 Flux 아키텍처를 제안했다.4 Flux의 핵심 철학은 ’단방향 데이터 흐름(unidirectional data flow)’이다.4 이는 데이터가 애플리케이션 내에서 항상 정해진 한 방향으로만 흐르도록 강제하여, 시스템의 동작을 예측 가능하게 만드는 것을 목표로 한다.2
Flux는 네 가지 주요 구성 요소로 이루어진다.
-
Action: 애플리케이션에서 발생하는 모든 변화를 기술하는 단순한 객체.
-
Dispatcher: 모든 액션을 받아 등록된 모든 스토어에 전달하는 중앙 허브.
-
Store: 애플리케이션의 상태와 상태 변경 로직을 포함하는 저장소. 오직 디스패처가 전달한 액션에 의해서만 상태를 변경할 수 있다.
-
View: 스토어의 데이터를 시각화하고, 사용자 상호작용에 따라 새로운 액션을 발생시키는 역할.
이 구조에서는 데이터 흐름이 View -> Action -> Dispatcher -> Store -> View라는 엄격한 단방향 순환 구조를 따른다.4 양방향 흐름을 원천적으로 차단함으로써, 개발자는 상태 변화의 원인과 결과를 명확하게 추적할 수 있게 되었다.
1.4 Redux의 정의 및 포지셔닝
Redux는 Flux 아키텍처의 개념을 계승하면서도, 이를 더욱 정제하고 발전시킨 가장 성공적인 구현체 중 하나이다.7 2015년 Dan Abramov와 Andrew Clark에 의해 개발된 Redux는 자바스크립트 애플리케이션을 위한 예측 가능한 상태 컨테이너(predictable state container)로 정의된다.8
Redux의 핵심 목표는 공식 문서에서 명시하듯 “시간 경과에 따른 상태 변화에 대한 이해를 돕는 것“이다.7 이를 달성하기 위해 Redux는 다음과 같은 네 가지 핵심 가치를 제공한다.
-
예측 가능성 (Predictable): 모든 상태 변화는 중앙에서 엄격한 순서에 따라 처리되며, 순수 함수에 의해 결정되므로 애플리케이션의 동작을 예측하기 쉽다.3
-
중앙 집중화 (Centralized): 애플리케이션의 모든 전역 상태가 단 하나의 스토어에 집중되어 있어, 데이터 관리가 용이하고 상태의 일관성을 유지하기 쉽다.3
-
디버깅 용이성 (Debuggable): 모든 상태 변경은 액션이라는 객체로 기록된다. 이를 통해 상태 변화의 전체 이력을 추적하고, 특정 시점의 상태를 재현하는 ’시간 여행 디버깅’과 같은 강력한 디버깅이 가능하다.3
-
유연성 (Flexible): Redux 자체는 매우 작은 라이브러리이지만, 미들웨어(middleware)라는 확장 가능한 구조를 통해 비동기 처리, 로깅 등 다양한 기능을 유연하게 추가할 수 있다.3
결론적으로, Redux의 탄생은 단순히 새로운 기술의 등장이 아니라, 현대 웹 애플리케이션의 본질적인 문제인 ’복잡성 제어’에 대한 철학적 해답을 제시한 사건이었다. 예측 불가능한 상태 변화로 인한 혼돈을 제어하기 위해 ’단방향 흐름’과 ’불변성’이라는 강력한 제약을 의도적으로 도입한 것이다. 따라서 Redux를 이해하는 것은 API 사용법을 배우는 것을 넘어, 복잡한 시스템에서 예측 가능성과 디버깅 용이성을 확보하기 위해 가해진 제약의 가치를 이해하는 과정이라 할 수 있다.
2. Redux를 관통하는 3대 원칙
Redux의 모든 아키텍처와 동작 방식은 세 가지의 근본적인 원칙 위에 세워져 있다. 이 원칙들은 서로 유기적으로 결합하여 Redux의 예측 가능성과 안정성을 보장하는 핵심적인 계약(contract) 역할을 한다.12
2.1 Single Source of Truth (진실은 하나의 근원으로부터)
첫 번째 원칙은 “애플리케이션의 모든 전역 상태는 단 하나의 스토어(Store) 내에 있는 하나의 객체 트리(object tree) 구조로 저장된다“는 것이다.12 여러 곳에 흩어져 있던 상태를 하나의 중앙 집중화된 저장소에서 관리함으로써, 데이터 흐름을 단순화하고 여러 가지 이점을 얻을 수 있다.
2.1.1 이점 분석
-
디버깅 용이성: 애플리케이션의 전체 상태가 하나의 자바스크립트 객체에 담겨 있기 때문에, 현재 시스템이 어떤 상태에 있는지 한눈에 파악하기 매우 용이하다. 이는 버그 발생 시 문제의 원인을 진단하는 데 결정적인 도움을 준다.15
-
상태 동기화 문제 해결: 여러 컴포넌트가 각자 상태의 복사본을 가지고 있을 때 발생할 수 있는 데이터 불일치 문제가 원천적으로 차단된다. 모든 컴포넌트는 항상 동일한 ’진실의 원천’을 참조하므로, 상태의 일관성이 보장된다.14
-
서버 렌더링 및 상태 지속성: 서버에서 애플리케이션의 초기 상태를 생성하여 클라이언트로 전달(serialize/hydrate)하는 과정이 매우 간단해진다. 또한, 애플리케이션의 현재 상태를 로컬 스토리지 등에 저장했다가 나중에 복원하는 기능(persistence)을 구현하기 용이하다.15
-
실행 취소/다시 실행 구현: 애플리케이션의 상태가 하나의 객체로 관리되므로, 이전 상태들을 스택에 저장해두는 방식으로 실행 취소(undo) 및 다시 실행(redo) 기능을 비교적 손쉽게 구현할 수 있다.15
2.2 State is Read-Only (상태는 읽기 전용이다)
두 번째 원칙은 “상태를 직접 변경하는 것은 금지되며, 오직 액션(Action)을 전달(dispatch)하는 것을 통해서만 상태 변경을 요청할 수 있다“는 것이다.12 이는 상태가 예측 불가능한 방식으로 변경되는 것을 막는 핵심적인 안전장치이다.
뷰(View)나 네트워크 콜백과 같은 코드에서 상태 객체를 직접 수정(mutation)하는 행위는 허용되지 않는다.15 대신, “어떤 일이 일어났는지“를 기술하는
type 속성을 가진 평범한 자바스크립트 객체인 액션을 생성하여 스토어에 전달해야 한다. 이 과정을 ’디스패치(dispatch)’라고 한다.14
2.2.1 불변성(Immutability)의 중요성
이 원칙은 데이터의 불변성(Immutability) 개념과 깊이 연결된다. 상태를 변경할 때는 기존 상태 객체를 직접 수정하는 것이 아니라, 항상 새로운 상태 객체를 생성하여 반환해야 한다.14 Redux는 상태 객체의 이전 버전과 새 버전의 참조(reference)를 비교하는 얕은 동등성 검사(shallow equality check)를 통해 변화를 감지하기 때문이다.20 만약 기존 객체를 직접 수정하면 참조가 동일하게 유지되므로, Redux는 상태가 변경되었음을 인지하지 못해 UI가 업데이트되지 않는 문제가 발생한다.10
모든 상태 변경이 액션을 통해 중앙에서 엄격한 순서에 따라 처리되므로, 여러 곳에서 동시에 상태를 변경하려 할 때 발생하는 경쟁 상태(race condition)와 같은 미묘한 버그를 방지할 수 있다.15
2.3 Changes are Made with Pure Functions (변화는 순수 함수로 작성되어야 한다)
세 번째 원칙은 “액션에 의해 상태가 어떻게 변하는지를 명시하기 위해 리듀서(Reducer)라는 순수 함수(Pure Function)를 작성해야 한다“는 것이다.12 리듀서는 Redux 아키텍처의 심장부로, 실제 상태 변경 로직을 담당한다.
리듀서는 이전 상태(previousState)와 액션(action)이라는 두 개의 인자를 받아 새로운 상태(newState)를 반환하는 함수이다.15 이를 수학적 상태 전이 함수로 표현하면 다음과 같다.
코드 스니펫
$$S_{n+1} = \text{Reducer}(S_n, A_n)$$
여기서 $S_n$은 시간 $n$에서의 상태, $A_n$은 발생한 액션, 그리고 $S_{n+1}$은 그 결과로 생성된 새로운 상태를 의미한다. 이 함수는 반드시 ’순수 함수’의 조건을 만족해야 한다.13
순수 함수의 수학적 정의와 특성
-
동일한 입력, 동일한 출력: 함수는 오직 입력으로 주어진 인자 값에만 의존해야 한다. 동일한
state와action이 주어지면, 외부 환경이나 시간에 관계없이 항상 동일한newState를 반환해야 한다.13 -
사이드 이펙트(Side Effects) 금지: 함수는 자신의 스코프 외부의 어떤 상태도 변경해서는 안 된다. API 호출,
console.log, 파일 시스템 접근, 라우팅 변경 등과 같은 부수 효과를 일으켜서는 안 된다. 이러한 작업은 미들웨어와 같은 다른 영역에서 처리해야 한다.13
이러한 순수 함수의 특성은 리듀서의 동작을 완벽하게 예측 가능하게 만들며, 테스트를 매우 용이하게 한다. 동일한 초기 상태와 액션만 주어지면 리듀서의 결과를 항상 검증할 수 있기 때문이다.
이 세 가지 원칙은 개별적으로도 중요하지만, 함께 작동할 때 Redux의 진정한 힘을 발휘한다. ’Single Source of Truth’는 모든 상태 스냅샷을 하나의 객체로 표현하게 하고, ’State is Read-Only’는 상태 변경의 유일한 원인이 기록 가능한 ’액션’임을 보장한다. 마지막으로 ’Changes are Made with Pure Functions’는 동일한 초기 상태와 액션 시퀀스가 주어지면 항상 동일한 최종 상태에 도달함을 수학적으로 보증한다. 이 세 가지 원칙의 결합은 애플리케이션의 전체 생명주기를 초기 상태와 액션들의 배열이라는 두 가지 요소로 완벽하게 기술할 수 있게 만든다. 이는 Redux DevTools의 강력한 기능인 ’시간 여행 디버깅(Time-travel Debugging)’을 가능하게 하는 이론적 기반이 된다.11 즉, 시간 여행 디버깅은 단순한 편의 기능이 아니라, Redux의 핵심 원칙들이 만들어낸 필연적인 결과물인 것이다.
제2장: Redux 아키텍처와 데이터 흐름의 해부
Redux는 세 가지 핵심 원칙을 구현하기 위해 명확하게 정의된 아키텍처와 엄격한 단방향 데이터 흐름을 따른다. 이 장에서는 Redux를 구성하는 핵심 요소들의 역할과 이들이 상호작용하여 데이터 흐름을 완성하는 과정을 단계별로 상세히 분석한다.
2.1. 핵심 구성 요소 심층 분석
Redux 아키텍처는 크게 스토어, 액션, 리듀서라는 세 가지 핵심 요소로 구성된다.
스토어 (Store)
스토어는 Redux 애플리케이션의 ’두뇌’와 같다. 애플리케이션의 전체 상태 트리를 감싸는 중앙 저장소 역할을 하며, 상태 관리에 필요한 모든 것을 담고 있는 객체이다.24
-
생성 및 역할:
createStore함수를 통해 생성되며, 애플리케이션 전체에 단 하나만 존재해야 한다는 원칙을 따른다.20 스토어는 상태를 보관하고, 상태에 대한 접근을 제어하며, 상태 변경을 관리하는 책임을 진다. -
핵심 메서드: 스토어는 다음과 같은 세 가지 핵심 메서드를 외부에 노출한다.26
-
getState(): 스토어가 현재 가지고 있는 상태 객체를 반환한다. 컴포넌트는 이 메서드를 통해 상태를 읽을 수 있다. -
dispatch(action): 상태 변경을 유발하는 유일한 방법이다. 액션 객체를 인자로 받아 리듀서에게 전달하는 역할을 한다. -
subscribe(listener): 상태가 변경될 때마다 호출될 콜백 함수(리스너)를 등록한다. UI 라이브러리는 이 메서드를 사용하여 스토어의 변경 사항을 감지하고 화면을 다시 그린다.
액션 (Action)
액션은 “무슨 일이 일어났는가“를 설명하는 정보를 담은 평범한 자바스크립트 객체이다.6 이는 상태 변경의 의도를 명시하는 유일한 수단이다.
-
구조: 모든 액션 객체는 반드시
type속성을 가져야 한다.type은 보통 문자열 상수로 정의되며, 어떤 종류의 변화가 일어났는지를 나타내는 고유 식별자 역할을 한다.25 -
페이로드 (Payload):
type외에 상태 변경에 필요한 추가 데이터를 담기 위해 관례적으로payload라는 속성을 사용한다.19 예를 들어, 새로운 할 일을 추가하는 액션은
{ type: 'ADD_TODO', payload: { text: 'Learn Redux' } }와 같은 형태를 가질 수 있다.
- 액션 생성자 (Action Creator): 매번 액션 객체를 직접 작성하는 것은 번거롭고 오류를 유발하기 쉽다. 이를 방지하기 위해 액션 객체를 생성하여 반환하는 함수인 ’액션 생성자’를 만들어 사용하는 것이 일반적이다.10 이는 코드의 재사용성을 높이고 일관성을 유지하는 데 도움을 준다.
리듀서 (Reducer)
리듀서는 dispatch된 액션을 실제로 처리하여 상태를 변경하는 로직을 담고 있는 순수 함수이다.3 리듀서는 Redux의 ’일꾼’에 해당하며, 이전 상태와 액션을 받아 새로운 상태를 만들어내는 책임을 진다.
-
함수 시그니처:
(state, action) => newState형태를 따른다. 첫 번째 인자로는 현재 상태를, 두 번째 인자로는 디스패치된 액션 객체를 받는다. -
로직 구현: 일반적으로
switch문이나if문을 사용하여 전달받은 액션의type에 따라 분기한다. 각case에서는 이전 상태와 액션의payload를 조합하여 반드시 새로운 상태 객체를 생성하여 반환해야 한다.13 만약 처리할 수 없는 액션 타입이 들어오면,
default 구문에서 기존 state를 그대로 반환해야 한다.
- 리듀서 조합: 애플리케이션의 규모가 커지면 모든 상태 변경 로직을 하나의 리듀서 함수에 담는 것은 비효율적이다. 대신, 상태 트리의 각 부분을 담당하는 여러 개의 작은 리듀서(슬라이스 리듀서)로 분리한다. 그리고 Redux가 제공하는
combineReducers유틸리티를 사용하여 이들을 하나의 루트 리듀서(root reducer)로 통합한다.15 이 과정에서 각 슬라이스 리듀서가 관리하는 상태 조각들이 모여 전체 상태 트리를 구성하게 된다.
2.2. 단방향 데이터 흐름 (Unidirectional Data Flow)의 전 과정
Redux의 모든 구성 요소는 엄격한 단방향 데이터 흐름을 따라 상호작용한다. 이 흐름은 예측 가능하고 추적하기 쉬운 애플리케이션 로직을 보장한다.6 전체 과정은 다음과 같은 단계로 이루어진다.
-
이벤트 발생 및
dispatch(action)호출: 사용자가 버튼을 클릭하는 등 UI와 상호작용하면, 해당 이벤트 핸들러는 액션 생성자를 호출하여 액션 객체를 만든다. 그리고 이 액션 객체를store.dispatch()함수의 인자로 전달하여 호출한다.6 이것이 상태 변경 사이클의 시작점이다. -
스토어의 액션 전파:
dispatch된 액션은 스토어로 전달된다. 스토어는 다른 로직을 수행하지 않고, 즉시 현재 상태 트리와 전달받은 액션을 두 인자로 삼아 루트 리듀서 함수를 호출한다.4 -
리듀서의 상태 계산: 루트 리듀서는 액션의
type을 확인하고, 해당 로직을 처리할 책임이 있는 하위 리듀서에게 작업을 위임한다. 하위 리듀서는 이전 상태 조각과 액션을 바탕으로 새로운 상태 조각을 계산하여 반환한다. 이 과정에서 불변성 원칙에 따라 기존 상태를 직접 수정하지 않고, 전개 구문(...state) 등을 사용하여 새로운 객체를 생성한다.19 -
새로운 상태로 스토어 업데이트: 모든 하위 리듀서가 새로운 상태 조각을 반환하면, 루트 리듀서는 이들을 조합하여 완전한 형태의 새로운 상태 트리를 만든다. 스토어는 리듀서로부터 이 새로운 상태 트리를 받아, 내부적으로 가지고 있던 이전 상태를 이 새로운 상태로 완전히 교체한다.19
-
리스너 호출 및 UI 업데이트: 상태 업데이트가 완료되면, 스토어는
subscribe메서드를 통해 등록된 모든 리스너 함수들을 순차적으로 호출한다.16
react-redux와 같은 UI 바인딩 라이브러리는 이 알림을 받아, 스토어의 새로운 상태를 구독하고 있는 컴포넌트들에게 변경 사실을 알린다.
- UI 리렌더링: 상태 변경에 영향을 받는 컴포넌트들은 새로운 props나 상태를 받아 리렌더링(re-rendering)을 수행한다. 이로써 변경된 상태가 사용자 화면에 반영되고, 데이터 흐름의 한 사이클이 완료된다.19
이처럼 Redux의 아키텍처는 ’무엇(What)’과 ’어떻게(How)’를 철저히 분리하는 ‘관심사의 분리(Separation of Concerns)’ 원칙을 극단적으로 적용한 결과물이다. UI 컴포넌트는 “무슨 일이 일어났는지“를 표현하는 액션을 디스패치할 뿐, 상태가 “어떻게 변해야 하는지“에 대해서는 전혀 알지 못한다.3 반면, 리듀서는 오직 “어떻게 상태를 변경할 것인가“에 대한 로직만을 책임진다. 이러한 철저한 분리는 각 구성 요소를 독립적으로 테스트하고 이해하기 쉽게 만들며, 대규모 팀에서의 협업과 코드의 장기적인 유지보수성을 극적으로 향상시킨다. Redux의 초기 설정이 다소 복잡하게 느껴지는 것은 바로 이 구조적 분리를 위한 비용인 셈이다.
제3장: 현대적 Redux 개발: Redux Toolkit (RTK)
Redux는 강력한 상태 관리 패턴을 제공하지만, 초기 버전의 사용 방식은 몇 가지 명백한 문제점을 안고 있었다. 이를 해결하고 개발자 경험을 현대화하기 위해 Redux 공식 팀은 Redux Toolkit(RTK)을 출시했다. RTK는 현재 Redux 로직을 작성하는 공식적으로 권장되는 표준 방식이다.21
3.1. “클래식” Redux의 문제점
RTK가 등장하기 이전의 “클래식” Redux 개발 방식은 다음과 같은 문제들로 인해 비판을 받았다.
-
과도한 보일러플레이트 코드 (Boilerplate Code): 가장 큰 문제점으로, 아주 간단한 기능을 하나 추가하기 위해서도 액션 타입(상수), 액션 생성자(함수), 리듀서(
switch구문)를 각각의 파일에 걸쳐 작성해야 했다.5 이러한 반복적인 코드는 개발 속도를 저하시키고 코드베이스를 불필요하게 비대하게 만들었다.30 -
복잡한 스토어 설정:
createStore함수를 사용하여 스토어를 설정하는 과정이 복잡했다. 비동기 처리를 위한 미들웨어(middleware) 적용, Redux DevTools 연동 등을 위해 여러 함수를 조합(compose,applyMiddleware)해야 했으며, 이는 초보자에게 높은 진입 장벽으로 작용했다.5 -
다수의 패키지 의존성: 실용적인 Redux 애플리케이션을 구축하기 위해서는 Redux 코어 라이브러리 외에도 여러 추가 패키지를 직접 설치하고 구성해야 했다. 비동기 로직을 위한
redux-thunk나redux-saga, 불변성 유지를 간소화하는immer, 메모이제이션된 셀렉터를 만들기 위한reselect등이 대표적이다.5
3.2. Redux Toolkit: “의견을 가진(Opinionated)” 도구 모음
Redux Toolkit은 위에서 언급한 문제들을 해결하기 위해 탄생한, “이것만으로도 작동하는(batteries-included)” 도구 모음이다.28 RTK는 Redux 사용에 대한 모범 사례(best practices)를 내장하고 있으며, 개발자가 흔히 저지르는 실수를 방지하고 더 적은 코드로 더 많은 작업을 수행할 수 있도록 돕는 것을 목표로 한다.28 “의견을 가졌다(Opinionated)“는 것은 개발자에게 특정 방식을 강력하게 권장함으로써, 고민의 여지를 줄이고 생산성을 높인다는 의미이다.
3.3. 핵심 API 심층 탐구
RTK는 Redux 개발의 전 과정을 단순화하는 여러 유틸리티 함수를 제공한다.
configureStore()
configureStore는 기존의 createStore를 추상화하여 스토어 설정을 획기적으로 간소화한다.28
-
자동화된 설정: 이 함수는 단 하나의 설정 객체를 인자로 받는다.
reducer필드에 각 기능별 리듀서(슬라이스 리듀서)들을 객체 형태로 전달하기만 하면, 내부적으로combineReducers를 자동으로 호출하여 루트 리듀서를 생성한다.29 -
기본 미들웨어 내장: 가장 널리 사용되는 비동기 미들웨어인
redux-thunk가 기본적으로 포함되어 있어 별도의 설정 없이 바로 비동기 로직을 작성할 수 있다.28 또한, 개발 환경에서는 상태 불변성 위반을 감지하는 미들웨어도 자동으로 추가된다. -
DevTools 연동: Redux DevTools Extension 연동이 자동으로 활성화되어, 복잡한 설정 없이도 강력한 디버깅 도구를 즉시 사용할 수 있다.28
createSlice()
createSlice는 RTK의 가장 핵심적인 기능으로, Redux의 보일러플레이트 문제를 근본적으로 해결한다. 이는 흩어져 있던 액션 타입, 액션 생성자, 리듀서를 하나의 함수 호출로 통합한다.28
-
통합된 로직:
createSlice는name,initialState,reducers세 가지 주요 속성을 가진 객체를 인자로 받는다.29 -
name: 슬라이스의 이름을 지정하며, 액션 타입 문자열의 접두사로 사용된다 (예:'counter/increment'). -
initialState: 해당 슬라이스의 초기 상태 값을 정의한다. -
reducers: 리듀서 로직을 담는 객체. 각 키는 액션의 이름이 되고, 값은 해당 액션을 처리하는 리듀서 함수가 된다. -
Immer 라이브러리 내장:
createSlice의 가장 혁신적인 부분은 내부적으로 Immer 라이브러리를 사용한다는 점이다.28 이 덕분에 리듀서 함수 내에서
state.value += 1과 같이 상태를 직접 수정하는 것처럼 보이는 “가변적(mutative)” 코드를 작성할 수 있다. Immer는 이 코드를 감지하여 실제로는 안전한 불변 업데이트(immutable update)를 수행하는 코드로 변환해준다. 이는 클래식 Redux에서 불변성을 지키기 위해 사용했던 복잡한 전개 구문(...state)을 완전히 대체하여 코드의 가독성을 극적으로 향상시킨다.34
- 자동 생성:
createSlice는reducers객체에 정의된 함수 이름을 기반으로 액션 생성자(slice.actions)와 전체 슬라이스 리듀서(slice.reducer)를 자동으로 생성하여 반환한다. 개발자는 이를export하여 컴포넌트와 스토어에서 사용하기만 하면 된다.29
createAsyncThunk()
createAsyncThunk는 API 요청과 같은 비동기 로직 처리를 위한 표준화된 패턴을 제공하여, redux-thunk를 직접 사용하는 것보다 더 구조화된 방식을 제시한다.28
-
액션 자동화: 이 함수는 두 개의 인자를 받는다. 첫 번째는 액션 타입을 식별하기 위한 고유한 문자열 접두사이고, 두 번째는 프로미스(Promise)를 반환하는 비동기 콜백 함수(payload creator)이다.35
-
생명주기 액션:
createAsyncThunk를 호출하면, 해당 비동기 작업의 진행 상태에 따라 세 가지 생명주기 액션을 자동으로dispatch하는 thunk 액션 생성자를 반환한다.35 -
pending: 비동기 작업이 시작되었음을 알리는 액션. -
fulfilled: 비동기 작업이 성공적으로 완료되었음을 알리는 액션.payload에는 프로미스의 결과값이 담긴다. -
rejected: 비동기 작업이 실패했음을 알리는 액션.payload나error객체에 에러 정보가 담긴다. -
extraReducers연동: 이렇게 자동으로 생성된 세 가지 액션 타입은createSlice의extraReducers필드에서 처리할 수 있다.extraReducers는 슬라이스 내부에서 정의되지 않은 외부 액션에 반응하는 로직을 작성하는 곳이다. 개발자는 여기에 각 생명주기 액션에 대한 리듀서를 작성하여 로딩 상태, 성공 시 데이터 저장, 실패 시 에러 처리 등을 손쉽게 구현할 수 있다.36
결론적으로, Redux Toolkit은 Redux의 근본 철학인 3대 원칙을 훼손하지 않으면서, 개발 과정에서 발생하는 불필요한 복잡성과 반복 작업을 자동화하여 개발자 경험(Developer Experience)을 극대화하는 방향으로 진화한 결과물이다. 클래식 Redux의 문제점이 3대 원칙을 지키기 위한 ’구현상의 비용’이었다면, RTK는 그 비용을 최소화함으로써 개발자가 저수준의 구현 디테일이 아닌, 애플리케이션의 핵심 비즈니스 로직에만 집중할 수 있도록 돕는다. 이것이 바로 Redux가 현대 프론트엔드 개발 환경에서 여전히 강력한 경쟁력을 유지하게 하는 가장 중요한 원동력이다.
제4장: React와 Redux의 유기적 통합
Redux 자체는 특정 UI 프레임워크에 종속되지 않는 순수 자바스크립트 라이브러리이다.38 따라서 React 애플리케이션에서 Redux를 효율적으로 사용하기 위해서는 둘을 연결해주는 ‘바인딩(binding)’ 라이브러리가 필요하다.
react-redux는 Redux 공식 팀에서 제공하는 공식 바인딩 라이브러리로, Redux 스토어를 React의 선언적 UI 모델과 유기적으로 통합하는 역할을 수행한다.40
4.1. react-redux 라이브러리의 역할
react-redux는 Redux 스토어의 상태 변화를 감지하여 필요한 React 컴포넌트만 효율적으로 리렌더링하는 복잡한 로직을 내부에 추상화하고 있다. 이를 통해 개발자는 스토어 구독(subscription) 관리와 성능 최적화에 대한 부담 없이, 선언적인 방식으로 Redux 상태를 컴포넌트에서 사용하고 업데이트할 수 있다.
4.2. Provider 컴포넌트
React 애플리케이션에 Redux 스토어를 ’주입’하는 첫 단계는 <Provider> 컴포넌트를 사용하는 것이다.
-
역할:
Provider는react-redux라이브러리에서 제공하는 특수 컴포넌트로, 애플리케이션의 최상위 컴포넌트(일반적으로App.js)를 감싸는 형태로 사용된다.29 -
구현:
storeprop으로 Redux 스토어 객체를 전달받는다.
JavaScript
import { Provider } from 'react-redux';
import store from './app/store';
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
- 동작 원리:
Provider는 내부적으로 React의 Context API를 사용하여, 컴포넌트 트리 내의 모든 하위 컴포넌트가props를 통해 명시적으로 전달받지 않아도 Redux 스토어에 접근할 수 있도록 만들어준다.7 이는 전역 상태에 접근하기 위한 통로를 열어주는 것과 같다.
4.3. React Hooks를 활용한 최신 연동 방식
과거 react-redux는 connect라는 고차 컴포넌트(HOC, Higher-Order Component)를 사용하여 컴포넌트를 스토어에 연결했다. 하지만 React Hooks가 도입된 이후로는 useSelector와 useDispatch라는 두 개의 훅을 사용하는 것이 훨씬 더 간결하고 직관적인 표준 방식으로 자리 잡았다.
useSelector()
useSelector 훅은 컴포넌트가 Redux 스토어의 상태 데이터 일부를 조회하고 ’구독(subscribe)’할 수 있게 해준다.19
- 사용법: 인자로 ‘셀렉터(selector)’ 함수를 받는다. 이 함수는 스토어의 전체 상태 객체(
state)를 인자로 받아, 해당 컴포넌트에서 필요한 특정 값만 추출하여 반환한다.43
JavaScript
import { useSelector } from 'react-redux';
const CounterComponent = () => {
const count = useSelector((state) => state.counter.value);
//...
};
- 자동 구독 및 리렌더링:
useSelector를 사용하면 컴포넌트는 셀렉터 함수가 반환하는 값의 변경 사항을 자동으로 구독한다.react-redux는 Redux 스토어의 상태가 변경될 때마다 셀렉터 함수를 다시 실행하고, 이전에 반환했던 값과 새로운 반환 값을 엄격한 동등성 비교(===)를 통해 비교한다. 만약 두 값이 다르면, 해당 컴포넌트를 자동으로 리렌더링하여 UI를 업데이트한다.33
이러한 동작 방식은 useSelector를 단순한 데이터 조회 도구를 넘어, Redux의 전역 상태와 React의 지역적 렌더링 사이의 ‘성능 브릿지’ 역할을 수행하게 한다. Redux 스토어에는 수많은 상태값이 존재하지만, useSelector는 각 컴포넌트가 “나는 전체 상태 중 오직 이 부분에만 관심이 있다“고 명확히 선언하게 한다. Redux의 불변성 원칙 덕분에, 리듀서는 상태 변경 시 관련 없는 부분의 객체 참조는 그대로 유지하고 변경된 부분만 새로운 객체로 만든다. 따라서 useSelector는 이 참조 비교를 통해 효율적으로 변화를 감지하고, 불필요한 리렌더링을 최소화하여 애플리케이션의 성능을 최적화할 수 있다. 즉, 잘 설계된 셀렉터를 작성하는 것만으로도 개발자는 상당한 성능 이점을 얻게 된다.
useDispatch()
useDispatch 훅은 컴포넌트 내에서 스토어의 dispatch 함수에 접근할 수 있게 해준다.19
-
사용법: 컴포넌트 내에서
const dispatch = useDispatch()와 같이 호출하여dispatch함수를 얻는다. -
액션 전달: 이후 이벤트 핸들러나
useEffect훅 등에서 이dispatch함수를 사용하여 액션을 스토어로 전달할 수 있다.
JavaScript
import { useDispatch } from 'react-redux';
import { increment } from './counterSlice';
const CounterComponent = () => {
const dispatch = useDispatch();
return (
<button onClick={() => dispatch(increment())}> Increment </button>
);
};
useDispatch는 컴포넌트가 상태 변경 로직을 직접 알 필요 없이, 단지 “이러한 액션을 실행해달라“고 요청만 할 수 있게 함으로써, Redux의 관심사 분리 원칙을 React 컴포넌트 레벨에서 효과적으로 구현하도록 돕는다.
제5장: 심화 주제 및 생태계 확장
Redux의 기본 아키텍처를 이해했다면, 이제 그 기능을 확장하고 대규모 애플리케이션에 적용하기 위한 심화 주제들을 살펴볼 차례이다. 미들웨어, 개발자 도구, 그리고 확장 가능한 상태 구조 설계는 Redux를 전문가 수준으로 활용하기 위한 필수적인 요소들이다.
5.1. 미들웨어 (Middleware)의 개념과 응용
미들웨어는 Redux 아키텍처의 유연성을 극대화하는 핵심적인 확장 메커니즘이다. 이는 액션이 dispatch된 후 리듀서에 도달하기 전의 중간 단계에 위치하는 ’파이프라인’과 같다.10 미들웨어를 통해 액션을 가로채서 로깅, 비동기 처리, 라우팅 등 다양한 부수 효과(side effects)를 수행할 수 있다.
주요 사용 사례
-
로깅 (Logging): 가장 간단하면서도 유용한 미들웨어는
redux-logger이다. 이 미들웨어는dispatch되는 모든 액션, 액션이 실행되기 전의 상태, 그리고 액션이 실행된 후의 상태를 브라우저 콘솔에 자동으로 기록해준다.42 이는 개발 과정에서 데이터 흐름을 시각적으로 추적하고 디버깅하는 데 큰 도움을 준다. -
비동기 처리 (Asynchronous Actions): Redux의 리듀서는 순수 함수여야 하므로, API 호출과 같은 비동기 작업을 직접 처리할 수 없다. 이 문제를 해결하기 위해 비동기 처리 미들웨어를 사용한다.
-
Redux Thunk: 가장 널리 사용되는 미들웨어 중 하나로, 액션 객체 대신 함수를
dispatch할 수 있게 해준다.25 이 함수(thunk)는
dispatch와 getState를 인자로 받아, 비동기 작업(예: API 요청)을 수행한 후 그 결과에 따라 새로운 액션을 dispatch할 수 있다. Redux Toolkit에는 redux-thunk가 기본적으로 내장되어 있다.31
- Redux Saga: 제너레이터(Generator) 함수를 사용하여 더 복잡하고 정교한 비동기 흐름을 관리하는 데 사용된다. 동시성 관리, 태스크 취소, 경쟁 상태 처리 등
redux-thunk보다 훨씬 강력한 기능을 제공하지만, 학습 곡선이 더 가파르다.27
미들웨어는 applyMiddleware 함수를 통해 스토어에 적용되며, 여러 미들웨어를 체인 형태로 연결하여 순차적으로 실행할 수도 있다.
5.2. Redux DevTools를 활용한 고급 디버깅
Redux DevTools는 Redux 생태계의 가장 강력한 자산 중 하나로 꼽히는 브라우저 확장 프로그램이다.30 이는 Redux 스토어의 내부를 실시간으로 들여다보고 상호작용할 수 있는 강력한 디버깅 환경을 제공한다.
-
핵심 기능:
-
액션 히스토리: 애플리케이션이 실행된 이후
dispatch된 모든 액션의 목록을 시간 순서대로 보여준다. -
상태 변화 추적: 각 액션을 선택하면, 해당 액션으로 인해 상태가 어떻게 변화했는지 diff 형태로 명확하게 확인할 수 있다.
-
상태 검사: 특정 시점의 전체 상태 트리를 검사하고 탐색할 수 있다.
-
시간 여행 (Time-travel) 디버깅: Redux DevTools의 가장 독보적인 기능이다. 개발자는 액션 히스토리 목록에서 특정 액션을 건너뛰거나, 과거의 특정 시점으로 애플리케이션의 전체 상태를 되돌릴 수 있다.11 UI 역시 해당 시점의 상태를 그대로 재현하기 때문에, 특정 버그가 어떤 액션 시퀀스에 의해 발생했는지 매우 쉽고 직관적으로 추적할 수 있다. 이는 Redux의 3대 원칙(단일 스토어, 읽기 전용 상태, 순수 리듀서)이 만들어낸 실질적인 결과물이다.
5.3. 확장 가능한 상태 구조 설계
애플리케이션의 규모가 커지면, 스토어의 상태 구조를 어떻게 설계하는지가 유지보수성에 큰 영향을 미친다. 특히 API로부터 받아온 중첩된(nested) 데이터를 효율적으로 관리하는 것이 중요하다.
상태 정규화 (State Normalization)
관계형 데이터베이스에서 데이터를 정규화하는 것과 유사하게, Redux 스토어의 상태도 정규화하는 것이 권장된다. 이는 중첩된 데이터 구조를 평탄하게(flat) 만들어 데이터 중복을 방지하고, 특정 항목의 업데이트를 용이하게 하는 패턴이다.
예를 들어, 블로그 포스트와 댓글 데이터를 다음과 같이 정규화할 수 있다.
-
기존 구조 (중첩):
[{ id: 'post1', author: {...}, comments: [{...}, {...}] }] -
정규화된 구조:
JSON
{
"posts": {
"entities": { "post1": { "id": "post1", "author": "user1", "comments": ["comment1", "comment2"] } },
"ids": ["post1"]
},
"comments": {
"entities": { "comment1": {...}, "comment2": {...} },
"ids": ["comment1", "comment2"]
},
"users": {... }
}
이 구조에서는 특정 댓글 하나를 수정하기 위해 전체 포스트 객체를 깊게 탐색할 필요 없이, comments.entities.comment1에 직접 접근하여 수정할 수 있다.
createEntityAdapter (RTK)
Redux Toolkit은 이러한 정규화된 상태를 쉽게 관리할 수 있도록 createEntityAdapter라는 유틸리티를 제공한다.28 이는 정규화된 데이터를 다루기 위한 CRUD(Create, Read, Update, Delete) 관련 리듀서 로직과 셀렉터 함수들을 자동으로 생성해준다. 개발자는
addOne, updateOne, removeOne 등 미리 생성된 리듀서를 사용하여 보일러플레이트 코드 없이 간편하게 정규화된 상태를 관리할 수 있다.
제6장: 상태 관리 패러다임 비교 분석
Redux는 오랫동안 React 상태 관리의 표준으로 여겨졌지만, 현재 프론트엔드 생태계에는 다양한 철학과 접근 방식을 가진 여러 상태 관리 솔루션이 존재한다. 프로젝트의 요구사항과 특성에 맞는 최적의 도구를 선택하기 위해서는 각 솔루션의 철학적 차이와 기술적 장단점을 명확히 이해하는 것이 중요하다.
6.1. Redux vs. React Context API
React에 내장된 Context API는 Redux의 대안으로 자주 언급되지만, 두 도구의 근본적인 목적과 설계 철학은 매우 다르다.
- 핵심 차이: 가장 중요한 차이점은 Context가 ‘상태 관리’ 도구가 아니라 ‘의존성 주입(Dependency Injection)’ 메커니즘이라는 점이다.7 Context는 데이터를 저장하거나 관리하는 로직을 포함하지 않으며, 단지 상위 컴포넌트에서 하위 컴포넌트로 데이터를 쉽게 전달하는 ‘파이프’ 역할을 할 뿐이다. 실제 상태 관리는
useState나 useReducer 훅을 통해 이루어진다.45 반면, Redux는 상태 저장, 업데이트 로직, 미들웨어, 개발자 도구 등을 모두 포함하는 완전한 ’상태 관리 프레임워크’이다.45
- 성능: Context의
Provider에 전달되는value가 변경되면, 해당 Context를 구독(useContext)하는 모든 하위 컴포넌트가 리렌더링된다. 중간에React.memo등을 사용하여 수동으로 최적화하지 않으면, 상태의 작은 변화만으로도 불필요한 대규모 리렌더링이 발생하여 성능 저하를 유발할 수 있다.45 반면,
react-redux는 useSelector를 통해 컴포넌트가 구독하는 상태 조각이 실제로 변경되었을 때만 리렌더링을 트리거하는 정교한 성능 최적화 메커니즘을 기본적으로 제공한다.7
- 사용 사례: Context API는 prop drilling을 피하기 위한 목적으로, 자주 변경되지 않는 정적인 데이터(예: 테마 정보, 사용자 인증 상태, 언어 설정 등)를 전역적으로 공유하는 데 매우 적합하다.45 반면, 여러 컴포넌트에서 빈번하게 업데이트되고 복잡한 비즈니스 로직을 포함하는 동적인 전역 상태를 관리해야 할 경우에는 Redux가 훨씬 더 강력하고 확장성 있는 솔루션을 제공한다.46
6.2. Redux vs. Zustand
Zustand는 Redux의 영향을 받았지만, 극도의 단순성과 최소한의 보일러플레이트를 지향하는 경량 상태 관리 라이브러리이다.
- 철학 및 API 설계: Zustand는 Redux와 마찬가지로 Flux 패턴에 기반을 두지만, 설정 과정을 대폭 간소화했다.48 가장 큰 차이점은
Provider 컴포넌트로 앱을 감쌀 필요가 없다는 것이다.50
create 함수 하나로 스토어를 생성하고, 반환된 훅을 어떤 컴포넌트에서든 직접 임포트하여 사용할 수 있다. 이로 인해 초기 설정이 매우 간결하고 직관적이다.51
-
보일러플레이트: Zustand는 액션 타입, 액션 생성자, 디스패치 등의 개념을 추상화하여, 스토어 내에서 상태와 상태 변경 함수를 함께 정의한다. 이는 Redux Toolkit의
createSlice보다도 더 적은 양의 코드를 요구한다.34 -
생태계 및 구조: Redux는 수십 년간 축적된 방대한 커뮤니티, 풍부한 미들웨어 생태계, 그리고 강력한 Redux DevTools를 자랑한다.5 반면, Zustand는 상대적으로 생태계가 작고, 구조에 대한 강제성이 없어 대규모 프로젝트에서는 상태 관리 패턴의 일관성을 유지하기 위한 팀 내의 규약이 중요해진다.34
6.3. Redux vs. MobX
MobX는 Redux와는 근본적으로 다른 패러다임을 채택한 상태 관리 라이브러리이다.
- 패러다임: Redux가 함수형 프로그래밍과 데이터의 불변성을 강조하는 반면, MobX는 객체지향 프로그래밍(OOP)과 반응형 프로그래밍(Reactive Programming)에 기반을 둔다.39 MobX의 핵심은 ‘관찰 가능한(Observable)’ 상태이다. 상태를
observable로 만들면, MobX는 그 상태가 언제 어디서 사용되고 변경되는지를 자동으로 추적한다.
-
상태 업데이트: Redux에서는 상태를 변경하기 위해 명시적으로 액션을
dispatch하고 리듀서를 통해 새로운 상태를 반환해야 하는 명시적인 과정을 거친다. 반면, MobX에서는action으로 감싸진 함수 내에서 관찰 가능한 상태를 직접 ’수정(mutate)’하면, 해당 상태를 구독하고 있는 모든 UI(observer로 감싸진 컴포넌트)가 자동으로 리렌더링된다.39 이 ‘마법 같은’ 자동화는 개발자에게 매우 직관적인 경험을 제공한다. -
보일러플레이트: MobX는 데코레이터(decorator)나 프록시(proxy)를 사용하여 상태와 뷰를 암묵적으로 연결하므로, Redux에 비해 작성해야 할 코드의 양이 현저히 적다.52 상태 변경 로직이 분산되어 있지 않고 상태와 함께 존재하기 때문에 OOP에 익숙한 개발자에게는 더 자연스럽게 느껴질 수 있다.
상태 관리 솔루션 비교 분석표
다음 표는 각 상태 관리 솔루션의 주요 특징을 다차원적으로 비교하여 요약한 것이다.
| 기준 | Redux (with RTK) | React Context API | Zustand | MobX |
|---|---|---|---|---|
| 핵심 패러다임 | 함수형, 불변성, Flux | 의존성 주입 | 함수형, 불변성, Flux | 객체지향, 반응형(Observable) |
| 데이터 흐름 | 엄격한 단방향 | 단방향 (Provider -> Consumer) | 단방향 | 파생(Derivation) 기반 자동 |
| 보일러플레이트 | 중간 (RTK로 대폭 감소) | 매우 낮음 | 매우 낮음 | 낮음 |
| 성능 최적화 | useSelector로 정밀 제어 | 수동 최적화 필요 (useMemo) | Selector로 자동 최적화 | 자동 (변화 감지) |
| 비동기 처리 | createAsyncThunk (내장) | 직접 구현 (e.g., useEffect) | 미들웨어 또는 직접 구현 | 내장 액션 (async/await) |
| 디버깅 도구 | 매우 강력함 (Redux DevTools) | React DevTools (제한적) | Redux DevTools 지원 | MobX DevTools |
| 생태계 및 커뮤니티 | 매우 큼 | (React 내장) | 성장 중 | 중간 크기 |
| 주요 사용 사례 | 대규모, 복잡한 상태, 예측 가능성 요구 | Prop drilling 방지, 정적 데이터 | 중소규모, 빠른 개발, 간결함 | OOP 선호, 복잡한 상태 파생 |
결론: Redux, 언제 그리고 어떻게 사용할 것인가
Redux는 강력하고 성숙한 상태 관리 솔루션이지만, 모든 프로젝트에 적합한 만병통치약은 아니다. Redux를 도입하기 전에 프로젝트의 요구사항과 특성을 신중하게 평가하여 그 타당성을 검토하는 과정이 필수적이다.
Redux 도입의 타당성 평가 기준
Redux 공식 문서와 여러 전문가들의 의견을 종합해 볼 때, 다음과 같은 상황에서 Redux 도입을 적극적으로 고려할 수 있다.16
-
상태의 복잡성: 애플리케이션의 여러 위치에서 공유되어야 하는 상태의 양이 많고, 이 상태들이 시간이 지남에 따라 빈번하게 업데이트되며, 상태를 업데이트하는 로직이 복잡할 때 Redux는 빛을 발한다.16 상태 간의 의존성이 복잡하게 얽혀 있어 중앙에서 일관되게 관리해야 할 필요성이 클 경우, Redux는 명확한 구조를 제공한다.
-
팀 규모와 협업: 여러 개발자가 동시에 참여하는 중대규모 이상의 프로젝트에서 Redux는 중요한 역할을 한다. 엄격한 구조와 규칙은 코드의 일관성을 유지하고, 상태 관리 로직을 표준화하여 협업을 용이하게 만든다.16 새로운 팀원이 프로젝트에 합류했을 때도, 데이터 흐름을 예측하고 이해하기 쉬워 적응 기간을 단축시킬 수 있다.
-
디버깅 요구사항: 상태 변화의 모든 과정을 명확하게 추적하고, 특정 시점의 버그를 재현해야 하는 높은 수준의 디버깅이 요구되는 애플리케이션(예: 금융 서비스, 복잡한 편집 도구)에서 Redux DevTools가 제공하는 ‘시간 여행 디버깅’ 기능은 대체 불가능한 가치를 제공한다.7
상태 관리 전략 수립을 위한 최종 제언
-
Redux가 최적의 선택인 시나리오:
-
대규모 엔터프라이즈급 애플리케이션.
-
상태 변화의 예측 가능성과 추적성이 비즈니스 로직의 핵심인 프로젝트.
-
서버 사이드 렌더링(SSR) 시 서버와 클라이언트 간의 상태 동기화가 중요한 경우.
-
강력한 개발 도구와 성숙한 생태계(미들웨어, 라이브러리)의 지원이 필수적인 경우.
-
대안을 고려해야 할 시나리오:
-
프로젝트의 규모가 작고 전역 상태의 복잡성이 낮은 경우.
-
상태 업데이트 로직이 단순하고, 대부분의 상태가 특정 컴포넌트 트리 내에서만 사용되는 경우.
-
빠른 프로토타이핑과 개발 속도가 최우선 순위인 프로젝트.
이러한 시나리오에서는 React의 내장 상태 관리 기능(useState, useReducer, Context API)이나 Zustand, Jotai와 같은 경량 라이브러리를 사용하는 것이 더 효율적일 수 있다.30 불필요한 복잡성을 도입하는 것은 오히려 생산성을 저해하는 ’오버 엔지니어링’이 될 수 있다.
Redux 생태계의 미래와 전망
한때 과도한 보일러플레이트로 비판받았던 Redux는 Redux Toolkit(RTK)의 등장으로 과거의 단점을 상당 부분 극복했다. RTK는 현대적인 개발 경험을 제공하며 Redux가 여전히 상태 관리 생태계에서 중요한 위치를 차지하고 있음을 증명했다.
더 나아가, RTK Query와 같은 데이터 페칭 및 캐싱 솔루션이 Redux 생태계에 통합되면서, Redux는 단순한 클라이언트 상태 관리를 넘어 서버 상태 관리 영역까지 그 영향력을 확장하고 있다.28 이는 Redux가 끊임없이 진화하고 있음을 보여주는 증거이다.
궁극적으로 “최고의” 상태 관리 라이브러리는 존재하지 않는다. 오직 특정 문제와 환경에 “가장 적절한” 도구가 있을 뿐이다.30 따라서 개발자는 각 도구의 철학과 장단점을 깊이 이해하고, 현재 진행하는 프로젝트의 요구사항, 팀의 기술 스택과 선호도, 그리고 미래의 확장 가능성을 종합적으로 고려하여 현명한 기술 선택을 해야 할 것이다. Redux는 그 선택지 중 여전히 가장 강력하고 신뢰할 수 있는 카드 중 하나로 남아있다.